<?php
/**
 * @author 刘国君
 * @version 1.0
 * @url https://github.com/ideawu/phpvoip/blob/master/net/SipMessage.php
 * 这个类用来作为信息的处理
 */

namespace app\Handel;

class SipMessage
{
    public $src_ip;
    public $src_port;
    public $dst_ip;
    public $dst_port;

    public $uri;

    public $method;
    public $code;
    public $reason;

    public $call_id;
    public $branch;
    public $cseq;
    public $cseq_method;
    public $from; // Contact
    public $to; // Contact
    public $via = null;
    public $contact = null; // 网络层地址, 但RFC中似乎并没有这样说？

    public $expires = null;
    public $auth;
    public $allow = array();

    // 未详细解析的 header, 每个元素是 pair [key, val]
    public $headers = array();
    public $content = '';
    public $content_type = null;
    public $content_length = 0;
    public $xml_body = '';
    public $subject='';
    private static $code_reasons = array(
        100 => 'Trying',
        180 => 'Ringing',
        200 => 'OK',
        400 => 'Bad Request',
        401 => 'Unauthorized',
        403 => 'Forbidden',
        404 => 'Not Found',
        405 => 'Method Not Allowed',
        423 => 'Interval Too Brief',
        481 => 'Call/Transaction Does Not Exist',
        486 => 'Busy Here',
        487 => 'Request Terminated',
    );
    public $ptzcmd = array(
        'right' => 'A50F01021F0000D6',
        'left' => 'A50F01021F0000D6',
        'down' => 'A50F0104001F00D8',
        'up' => 'A50F0108001F00DC',
        'big' => 'A50F0110000010D5',
        'small' => 'A50F0120000010E5',
    );

    // 返回简洁描述
    function brief(){
        if($this->is_request()){
            $cmd = $this->method;
            $src = $this->from->username;
            $dst = $this->to->username;
        }else{
            $cmd = $this->code;
            if($this->code == 200){
                $cmd .= ' OK';
            }
            $src = $this->to->username;
            $dst = $this->from->username;
        }
        $ret = sprintf('%-8s %4d %-4s=>%-4s', $cmd, $this->cseq, $src, $dst);
        return $ret;
    }

    function is_request(){
        return $this->code == 0;
    }

    function is_response(){
        return !$this->is_request();
    }

    function add_header($key, $val){
        $this->headers[] = array($key, $val);
    }

    function get_header($key){
        foreach($this->headers as $v){
            if($key === $v[0]){
                return $v[1];
            }
        }
        return null;
    }

    function get_headers($key){
        $ret = array();
        foreach($this->headers as $v){
            if($key === $v[0]){
                $ret[] = $v[1];
            }
        }
        return $ret;
    }

    function del_header($key){
        foreach($this->headers as $index=>$v){
            if($key === $v[0]){
                unset($this->headers[$index]);
            }
        }
    }

    function encode(){
        if($this->code > 0 && strlen($this->reason) == 0){
            if(isset(self::$code_reasons[$this->code])){
                $this->reason = self::$code_reasons[$this->code];
            }
        }

        $headers = array();
        if($this->is_request()){
            $headers[] = "{$this->method} " . $this->uri->encode() . " SIP/2.0";
        }else{
            $headers[] = "SIP/2.0 {$this->code} {$this->reason}";
        }
        $headers[] = "From: " . $this->from->encode();
        $headers[] = "To: " . $this->to->encode();
        $headers[] = "Call-ID: {$this->call_id}";
        $headers[] = "CSeq: {$this->cseq} " . ($this->cseq_method? $this->cseq_method : $this->method);

        if($this->via){
            $headers[] = "Via: {$this->via}";
        }else{
            if($this->is_request()){
                $headers[] = "Via: SIP/2.0/UDP {$this->src_ip}:{$this->src_port};rport;branch={$this->branch}";
            }else{
                $headers[] = "Via: SIP/2.0/UDP {$this->dst_ip}:{$this->dst_port};rport;branch={$this->branch};received={$this->src_ip}";
            }
        }
        if($this->contact){
            $headers[] = "Contact: " . $this->contact->encode();
        }
        if($this->auth){
            if($this->is_request()){
                $headers[] = "Authorization: " . SIP::encode_www_auth($this->auth);
            }else{
                $headers[] = "WWW-Authenticate: " . SIP::encode_www_auth($this->auth);
            }
        }
        $ua = null;
        foreach($this->headers as $v){
            if($v[0] === 'Server' || $v[0] === 'User-Agent'){
                $ua = $v[0];
            }
            $headers[] = "{$v[0]}: {$v[1]}";
        }
        if(!$ua){
            $headers[] = "User-Agent: phpvoip";
        }

        $this->content_length = strlen($this->content);
        if($this->expires !== null){
            $headers[] = "Expires: {$this->expires}";
        }
        if($this->content_type){
            $headers[] = "Content-Type: " . $this->content_type;
        }
        if(!empty($this->subject)){
            $headers[] = "Subject: " . $this->subject;
        }
//        $headers[] = "Content-Length: " . $this->content_length;

        $xml_body = self::create_xml();
//        $headers[] = "Content-Length:  ".strlen($xml_body);
        $headers[] = "Content-Length:  ".strlen($this->content);
//        $ret = join("\r\n", $headers) . "\r\n\r\n{$xml_body}";
        $ret = join("\r\n", $headers) . "\r\n\r\n{$this->content}";
        return $ret;
    }
    //生成xml
    public static function create_xml(){
        $xml_body = "<? xml version=\"1.0\"?>
<Control>
<CmdType>DeviceControl</CmdType>
<SN>11</SN>
<DeviceID>34020000001320020191</DeviceID>
<PTZCmd> A50F4D1000001021</PTZCmd>
<Info>
<ControlPriority>5</ControlPriority>
</Info>
</Control>";
        return $xml_body;
    }


    // 返回解析的字节数，支持流式解析
    function decode($buf){
        list($header,$body)=explode("\r\n\r\n",$buf,2);
        $lines = explode("\r\n", $header);
        $first_line=array_shift($lines);
        $this->parse_first_line($first_line);

        foreach ($lines as $line){
            $this->parse_header_line($line);
        }
        //得到xml消息体
//        file_put_contents("xml_body.xml", $body);
        $xml_body = self::splicing_xml("xml_body.xml");
        $this->content = $body;
        $this->xml_body = $xml_body;
    }
    //解析xml
    public static function splicing_xml($xml_body){
        $msg_body = [];
        $xml = simplexml_load_file($xml_body);
        foreach($xml->children() as $child)
        {
            $msg_body[$child->getName()] = (array)$child;
        }
        return $msg_body;
    }
    private function parse_first_line($line){
        $ps = explode(' ', trim($line), 3);
        if(count($ps) != 3){
            return false;
        }
        if($ps[0] == 'SIP/2.0'){
            // is response
            $this->code = intval($ps[1]);
            $this->reason = $ps[2];
        }else{
            $this->method = $ps[0];
            $uri = new SipUri();
            $uri->decode($ps[1]);
            $this->uri = $uri;
        }
    }
    private function parse_header_line($line){
        $ps = explode(':', $line, 2);
        if(count($ps) != 2){
            // bad header line
            return;
        }
        $key = trim($ps[0]);
        $val = trim($ps[1]);
        // TODO: case insensitive
        if($key == 'From'){
            $this->from = SipContact::from_str($val);
        }else if($key == 'To'){
            $this->to = SipContact::from_str($val);
        }else if($key == 'Contact'){
            // TODO: support contact list
            $ps = explode(',', $val);
            $this->contact = SipContact::from_str($ps[0]);
        }else if($key == 'Call-ID'){
            $this->call_id = $val;
        }else if($key == 'CSeq'){
            $ps = explode(' ', $val);
            $this->cseq = intval($ps[0]);
            $this->cseq_method = $ps[1];
        }else if($key == 'Via'){
            $ret = $this->parse_via($val);
            $this->via_ip = $ret['ip'];
            $this->via_port = $ret['port'];
            if(isset($ret['tags']['branch'])){
                $this->branch = $ret['tags']['branch'];
            }
            $this->via = $val;
        }else if($key == 'Expires'){
            $this->expires = intval($val);
        }else if($key == 'Content-Length'){
            $this->content_length = intval($val);
        }else if($key == 'WWW-Authenticate'){
            $this->auth = SIP::decode_www_auth($val);
        }else if($key == 'Authorization'){
            $this->auth = SIP::decode_www_auth($val);
        }else if($key == 'Allow'){
            $this->allow = preg_split('/[, ]+/', trim($val));
        }else if($key == 'Subject'){
            $this->subject = Invite::decode_subject($val);
        }else{
            $this->headers[] = array($key, $val);
        }
    }

    private function parse_via($str){
        $ret = array(
            'ip' => '',
            'port' => 0,
            'tags' => array(),
        );

        $attrs = explode(';', $str);
        $p_a = explode(' ', $attrs[0], 2);
        if(isset($p_a[1])){
            $i_p = explode(':', $p_a[1]);
            $ret['ip'] = $i_p[0];
            $ret['port'] = isset($i_p[1])? intval($i_p[1]) : 0;
        }

        for($i=1; $i<count($attrs); $i++){
            $p = $attrs[$i];
            $kv = explode('=', $p);
            $ret['tags'][$kv[0]] = isset($kv[1])? $kv[1] : null;
        }

        if(isset($ret['tags']['rport']) && $ret['tags']['rport']){
            $ret['port'] = intval($ret['tags']['rport']);
        }

        return $ret;
    }
}